/**
 * Applikationsutveckling i Java, 5DV135
 * HT12
 * Project
 *
 * File: nullInvader.Engine.MapManager.java
 * Description: A manager that generate and stores maps as it's
 * main function. The MapManager also validate the maps, the
 * MapManager will cast exceptions in case the map
 * creation was not correct.
 * Date: 13 December 2012
 *
 * cs-user: dv11rlm
 * @author Robert Lundström
 */
package nullInvader.Engine;

import java.awt.Point;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class MapManager {

    private static final String DEFAULT_LEVELS_XML = "/nullInvader/Resources/levels.xml";
    private static final String LEVELS_XML_SCHEMA_XSD_PATH =
        "/nullInvader/Resources/levelsXMLschema.xsd";
    protected ConcurrentHashMap<Point, Zone> currenthZoneMap;
    protected ArrayList<Map> availableMaps;
    protected ArrayList<String> asciiMap;
    protected ArrayList<ArrayList<String>> oldAsciiMapList;
    protected ArrayList<String> backupDataList;
    protected ArrayList<String> asciiMapList;
    protected ArrayList<Point> goalList;
    protected ArrayList<Tower> towerList;
    protected int mapCounter;
    protected String mapFileName;
    protected Point start;

    /**
     * intialize used important class variables.
     *
     * @param fileName
     * @throws Exception
     */
    public MapManager(String fileName) throws Exception {
    mapCounter = 0;
    availableMaps = new ArrayList<Map>();
    asciiMap = new ArrayList<String>();
    asciiMapList = new ArrayList<String>();
    oldAsciiMapList = new ArrayList<ArrayList<String>>();
    currenthZoneMap = new ConcurrentHashMap<Point, Zone>();
    backupDataList = new ArrayList<String>();
    goalList = new ArrayList<Point>();
    mapFileName = "/nullInvader/Resources/" + fileName;
    parseMaps();
    generateMaps(parseXmlDom(mapFileName), "");
    }

    public MapManager() throws Exception {
    mapCounter = 0;
    availableMaps = new ArrayList<Map>();
    asciiMap = new ArrayList<String>();
    asciiMapList = new ArrayList<String>();
    oldAsciiMapList = new ArrayList<ArrayList<String>>();
    currenthZoneMap = new ConcurrentHashMap<Point, Zone>();
    backupDataList = new ArrayList<String>();
    goalList = new ArrayList<Point>();
    mapFileName = DEFAULT_LEVELS_XML;
    parseMaps();
    generateMaps(parseXmlDom(mapFileName), "");
    }

    /**
     * returns the next map.
     *
     * @return Map
     */
    public Map getNextMap() {
    mapCounter++;
    return availableMaps.get(mapCounter);
    }

    /**
     * returns current map
     *
     * @return Map
     */
    public Map getMap() {
    return availableMaps.get(mapCounter);
    }

    /**
     * will try and validate xml schema.
     *
     * @throws Exception
     */
    protected void parseMaps() throws Exception {
    Schema schema = loadSchema(LEVELS_XML_SCHEMA_XSD_PATH);
    Document document = parseXmlDom(mapFileName);
    validateXml(schema, document);
    }

    /**
     * validate the xml based on the xml-schema.
     *
     * @param schema
     * @param document
     * @throws Exception
     */
    protected void validateXml(Schema schema, Document document)
        throws Exception {
    try {
        // creating a Validator instance
        Validator validator = schema.newValidator();

        // validating the document against the schema
        validator.validate(new DOMSource(document));

    } catch (Exception e) {
        throw new IOException(
            "Unable to validate xml using xmlSchema");
    }
    }

    /**
     *
     * @param xmlSchemaFileName
     * @return schema
     * @throws Exception
     */
    protected Schema loadSchema(String xmlSchemaFileName)
        throws Exception {
    Schema schema = null;
    try {
        String language = XMLConstants.W3C_XML_SCHEMA_NS_URI;
        SchemaFactory factory = SchemaFactory
            .newInstance(language);
        schema = factory.newSchema(getClass().getResource(
            (xmlSchemaFileName)));
    } catch (Exception e) {
        throw new Exception("unable to load Schema");
    }
    return schema;
    }

    /**
     * fills a document with data from specified file.
     *
     * @param xmlFileName
     * @return Document
     * @throws IOException
     */
    protected Document parseXmlDom(String xmlFileName)
        throws Exception {
    Document document = null;
    DocumentBuilderFactory factory = DocumentBuilderFactory
        .newInstance();
    DocumentBuilder builder = null;
    try {
        builder = factory.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
        throw new IOException(e);
    }
    try {
        document = builder
            .parse(getClass()
                .getResourceAsStream(
                    ((xmlFileName))));
    } catch (Exception e) {
        throw new Exception("Unable to parse xml-file.");
    }
    return document;
    }

    /**
     * A recursive loop to go through the Node list.
     *
     * @param rootNode
     * @param previousData
     * @throws Exception
     */
    public void generateMaps(Node rootNode, String previousData)
        throws Exception {
    /*
     * if the node got no value we have no interest of it. the
     * node should also not just contain tabs.
     */
    if (rootNode.getNodeValue() != null
        && !rootNode.getNodeValue().contains("\t")) {
        /*
         * save the data read also remember what node we read the
         * data from as it's important in later parsing.
         */
        previousData += rootNode.getParentNode().getNodeName()
            + rootNode.getNodeValue();
        asciiMap.add(previousData);
        /*
         * startCash is the final tag in the map that we care
         * about so after that we have a new map or end of file.
         */
        if (previousData.startsWith("startCash")) {
        addMapToAvalibleMaps();
        }
        previousData = "";
    }
    // go through all the child-nodes.
    NodeList nodeList = rootNode.getChildNodes();
    for (int i = 0; i < nodeList.getLength(); i++)
        generateMaps(nodeList.item(i), previousData);
    }

    /**
     * read data from specified map.
     *
     * @param node
     * @param previousData
     * @param mapName
     * @throws Exception
     */
    public void generateNewMap(Node node, String previousData,
        String mapName) throws Exception {
    if (node.getNodeValue() != null
        && !node.getNodeValue().contains("\t")) {
        previousData += node.getParentNode().getNodeName()
            + node.getNodeValue();
        backupDataList.add(previousData);

        if (previousData.startsWith("startCash")) {
        getSpecialZone(backupDataList);
        }
        previousData = "";
    }
    NodeList nodeList = node.getChildNodes();
    for (int i = 0; i < nodeList.getLength(); i++) {
        generateNewMap(nodeList.item(i), previousData, mapName);
    }
    }

    /**
     * Function that calls other function to generate a map.
     *
     * @throws Exception
     */
    protected void addMapToAvalibleMaps() throws Exception {

    int startCash = 0;
    int goalScore = 0;
    ArrayList<UnitInfo> mapUnit = new ArrayList<UnitInfo>();
    ArrayList<String> specialCharacter = new ArrayList<String>();
    String tempUnitName = "";
    String tempZoneChar = null;
    String tempZoneClassName = null;
    String mapName = "";
    for (String s : asciiMap) {
        if (s.startsWith("row")) {
        asciiMapList.add(s.substring(3));
        } else if (s.startsWith("startCash")) {
        startCash = Integer.parseInt(s.substring(9));
        } else if (s.startsWith("mapChar")) {
        tempZoneChar = s.substring(7);
        } else if (s.startsWith("className")) {
        tempZoneClassName = s.substring(9);
        } else if (s.startsWith("moveable")) {
        specialCharacter.add(tempZoneChar + ":"
            + tempZoneClassName + ":" + s.substring(8));
        } else if (s.startsWith("goalScore")) {
        goalScore = Integer.parseInt(s.substring(9));
        } else if (s.startsWith("name")) {
        tempUnitName = s.substring(4);
        } else if (s.startsWith("mapName")) {
        mapName = s.substring(7);
        } else if (s.startsWith("price")) {
        mapUnit.add(new UnitInfo(tempUnitName, Integer
            .parseInt(s.substring(5))));
        }
    }

    generateZoneMap(specialCharacter);
    // add maps to mapList
    availableMaps.add(new Map(currenthZoneMap, mapName,
        goalScore, startCash, start, goalList, mapUnit,
        towerList));
    oldAsciiMapList.add(asciiMapList);
    asciiMap = new ArrayList<String>();
    asciiMapList = new ArrayList<String>();
    }

    /**
     * re-make the map at the current position(kartCounter).
     *
     * @throws Exception
     */
    public void resetCurrentMap() throws Exception {
    /*
     * check if the size of asciiMap is 0 so that we don't get
     * null from methods.
     */
    if (oldAsciiMapList.size() > 0) {
        // set the ascii map to the one we want to use.
        asciiMapList = oldAsciiMapList.get(mapCounter);
        generateNewMap(parseXmlDom(mapFileName), "",
            availableMaps.get(mapCounter).getName());

        Map tempMap = new Map(currenthZoneMap, availableMaps.get(
            mapCounter).getName(), availableMaps.get(
            mapCounter).getGoalScore(), availableMaps.get(
            mapCounter).getStartCash(), availableMaps.get(
            mapCounter).getStart(), availableMaps.get(
            mapCounter).getGoal(), availableMaps.get(
            mapCounter).getUnitPrices(), towerList);

        availableMaps.set(mapCounter, tempMap);
    }
    }

    /**
     *
     * @param localData
     * @throws Exception
     */
    protected void getSpecialZone(ArrayList<String> localData)
        throws Exception {
    String tempZoneChar = null;
    String tempZoneClassName = null;
    ArrayList<String> specialCharacter = new ArrayList<String>();
    for (String s : localData) {
        if (s.startsWith("mapChar")) {
        tempZoneChar = s.substring(7);
        } else if (s.startsWith("className")) {
        tempZoneClassName = s.substring(9);
        } else if (s.startsWith("moveable")) {
        specialCharacter.add(tempZoneChar + ":"
            + tempZoneClassName + ":" + s.substring(8));
        }
    }
    generateZoneMap(specialCharacter);
    }

    /**
     * create all the objects used in the map.
     *
     * @param specialCharacterList
     * @throws Exception
     */
    protected void generateZoneMap(
        ArrayList<String> specialCharacterList) throws Exception {
    currenthZoneMap = new ConcurrentHashMap<Point, Zone>(
        asciiMapList.size() * 2);
    Rock rock = new Rock(false);
    Road road = new Road(true);
    Goal goal = new Goal(true);
    HashMap<String, Field> specialZones =
        initializeXMLSpecifiedClasses(specialCharacterList);
    ArrayList<Point> towerPointList = new ArrayList<Point>();
    for (int row = 0; row < asciiMapList.size(); row++) {
        for (int colum = 0; colum < asciiMapList.get(row)
            .length(); colum++) {
        // if stone
        if (asciiMapList.get(row).charAt(colum) == '*') {
            currenthZoneMap.put(new Point(colum, row),
                new Zone(rock));
        }
        // if grass
        else if (asciiMapList.get(row).charAt(colum) == ' ') {
            currenthZoneMap.put(new Point(colum, row),
                new Zone(road));
        } else if (asciiMapList.get(row).charAt(colum) == 'J') {
            String direction = decideBrickType(specialZones,
                colum, row);
            if (!direction.equals("")) {
            currenthZoneMap
                .put(new Point(colum, row),
                    new Zone(new Junction(true,
                        direction)));
            } else {
            currenthZoneMap.put(new Point(colum, row),
                new Zone(road));
            }
        }
        // if char is a tower
        else if (asciiMapList.get(row).charAt(colum) == 'T') {
            towerPointList.add(new Point(colum, row));
        }
        // if char is a startPosition
        else if (asciiMapList.get(row).charAt(colum) == 'S') {
            start = new Point(colum, row);
            currenthZoneMap.put(new Point(colum, row),
                new Zone(road));
        }
        // if char is a goalPosition
        else if (asciiMapList.get(row).charAt(colum) == 'G') {
            goalList.add(new Point(colum, row));
            currenthZoneMap.put(new Point(colum, row),
                new Zone(goal));
        }
        // if special char
        else {
            Field oldBrick = specialZones.get(Character
                .toString(asciiMapList.get(row).charAt(
                    colum)));
            if (oldBrick != null) {
            currenthZoneMap.put(new Point(colum, row),
                new Zone(oldBrick));
            }
        }
        }
    }
    generateNewTowers(towerPointList);
    }

    /**
     * create WatchTowers.
     *
     * @param towerPointList
     */
    protected void generateNewTowers(ArrayList<Point> towerPointList) {
    towerList = new ArrayList<Tower>();
    for (Point p : towerPointList) {
        WatchTower tempTower = new WatchTower(false, p);
        tempTower.setReachableZones(getTowerReachables(p,
            tempTower));
        towerList.add(tempTower);
        currenthZoneMap.put(p, new Zone(tempTower));
    }
    }

    /**
     *
     * @param specialCharacter
     * @return
     * @throws Exception
     */
    protected HashMap<String, Field> initializeXMLSpecifiedClasses(
        ArrayList<String> specialCharacter) {
    HashMap<String, Field> specialZones = new HashMap<String, Field>();
    for (String s : specialCharacter) {
        String temp[] = s.split(":");
        Field bricks = null;
        if (temp.length != 3) {
        continue;
        }
        try {
        bricks = createBrickClass(temp[1], temp[2]);
        } catch (Exception e) {
        bricks = new Road(true);
        } finally {
        if (bricks != null) {
            specialZones.put(temp[0].toString(), bricks);
        }
        }
    }
    return specialZones;
    }

    /**
     * creates a new object that implements a interface or has a
     * parent that implements a the interface Field
     *
     * @param className
     * @param isMoveable
     * @return Field
     * @throws Exception
     */
    @SuppressWarnings("rawtypes")
    public Field createBrickClass(String className, String isMoveable)
        throws Exception {

    @SuppressWarnings("unchecked")
    Class<Bricks> newBrick = (Class<Bricks>) Class
        .forName("nullInvader.Engine." + className);
    // check if class is implementing the Field interface.
    for (Class<?> classInterface : newBrick.getSuperclass()
        .getInterfaces()) {
        if ("nullInvader.Engine.Field".equals(classInterface
            .getName())) {
        Constructor newB = newBrick
            .getConstructor(boolean.class);
        Object b = newB.newInstance(Boolean
            .parseBoolean(isMoveable));
        return (Field) b;
        }
    }
    // check if the class parent implements the Field interface.
    for (Class<?> classInterface : newBrick.getInterfaces()) {
        if ("nullInvader.Engine.Field".equals(classInterface
            .getName())) {
        Constructor newB = newBrick
            .getConstructor(boolean.class);
        Object b = newB.newInstance(Boolean
            .parseBoolean(isMoveable));
        return (Field) b;
        }
    }
    return null;
    }

    /**
     * check if the position should be a junction or a road. The check
     * involves looking at the ascii-maps positions around the current
     * position to check if the position is movable or not.
     *
     * @param specialZones
     *
     * @param colum
     * @param row
     * @return String Brick type
     */
    protected String decideBrickType(
        HashMap<String, Field> specialZones, int colum, int row) {
    boolean east = false;
    boolean west = false;
    boolean north = false;
    boolean south = false;
    // Create a position of current position.
    Point p = new Point(colum, row);
    // move the position to east of the current position.
    p.move(p.x + 1, p.y);
    /*
     * check if the position is within the ArrayList, if its ascii
     * value is ' ' this means it's a road object thus move-able
     * then check to see if there is an object on the map that is
     * move-able. If it's a move-able object we may walk to the
     * position because of this we may say that the unit on the
     * position mat walk to the position next to it.
     */
    if ((colum + 1 < asciiMapList.get(row).length())
        && (asciiMapList.get(row).charAt(colum + 1) == ' '
        || (currenthZoneMap
            .get(p) != null && currenthZoneMap.get(p)
            .getBrick().isMovable()))) {
        east = true;
    }
    /*
     * look to see if the object is a specially declared object
     * from the xml-file if so get infomration if it's move-able.
     */
    else if ((colum + 1 < asciiMapList.get(row).length())) {
        Field eastPos = specialZones.get(asciiMapList.get(row)
            .charAt(colum + 1));

        if (eastPos != null && eastPos.isMovable()) {
        east = true;
        }
    }
    p.move(p.x - 2, p.y);
    if ((colum - 1 > 0)
        && (asciiMapList.get(row).charAt(colum - 1)
            == ' ' || (currenthZoneMap
            .get(p) != null && currenthZoneMap.get(p)
            .getBrick().isMovable()))) {
        west = true;
    } else if (colum - 1 > 0) {
        Field westPos = specialZones.get(asciiMapList.get(row)
            .charAt(colum - 1));
        if (westPos != null && westPos.isMovable()) {
        west = true;
        }
    }
    p.move(p.x + 1, p.y + 1);
    if ((row + 1 < asciiMapList.size())
        && (asciiMapList.get(row + 1).charAt(colum) == ' '
        || (currenthZoneMap
            .get(p) != null && currenthZoneMap.get(p)
            .getBrick().isMovable()))) {
        south = true;
    } else if (row + 1 < asciiMapList.size()) {
        Field southPos = specialZones.get(asciiMapList.get(
            row + 1).charAt(colum));
        if (southPos != null && southPos.isMovable()) {
        south = true;
        }
    }
    p.move(p.x, p.y - 2);
    if ((row - 1 > 0)
        && (asciiMapList.get(row - 1).charAt(colum) == ' '
        || (currenthZoneMap
            .get(p) != null && currenthZoneMap.get(p)
            .getBrick().isMovable()))) {
        north = true;
    } else if (row - 1 > 0) {
        Field northPos = specialZones.get(asciiMapList.get(
            row - 1).charAt(colum));
        if (northPos != null && northPos.isMovable()) {
        south = true;
        }
    }
    /*
     * @see nullInvader.Engine.Junction()
     */
    if (east && north && south && west) {
        return "";
    } else if (west && east && south) {
        return "east";
    } else if (west && east && north) {
        return "west";
    } else if (west && south && north) {
        return "north";
    } else if (east && south && north) {
        return "south";
    } else {
        return "";
    }
    }

    /**
     * returns an array of reachable zones for the turret at position.
     *
     * @param position
     * @param watchTower
     * @return ArrayList<Zone>
     */
    protected ArrayList<Zone> getTowerReachables(Point position,
        WatchTower watchTower) {
    int range = watchTower.getRange();
    ArrayList<Point> allPositions = new ArrayList<Point>();
    for (int x = position.x - range; x < position.x; x++) {
        for (int i : getColumPositions(
            (range + 1 - (position.x - x)), position.y)) {
        allPositions.add(new Point(x, i));
        }
    }
    for (int x = position.x; x < position.x + range; x++) {
        for (int i : getColumPositions((position.x + range - x),
            position.y)) {
        allPositions.add(new Point(x, i));
        }
    }
    ArrayList<Zone> zoneList = new ArrayList<Zone>();
    for (Point currentPosition : allPositions) {
        Zone tempZone = currenthZoneMap.get(currentPosition);
        if (tempZone != null) {
        zoneList.add(tempZone);
        }
    }

    return zoneList;
    }

    /**
     * help method to getTowerReachables that returns positions for
     * turret.
     *
     * @see nullInvader.Engine.MapManager.getTowerReachables(Point
     *      position, WatchTower watchTower)
     * @param range
     * @param y
     * @return Integer[] positions
     */
    protected int[] getColumPositions(int range, int y) {

    int currentPosition = 0;
    if (range == 1) {
        int positions[] = new int[range];
        positions[currentPosition] = y;
        return positions;
    } else {
        int positions[] = new int[range];
        for (int pos = y - range / 2; pos < y + range / 2; pos++) {
        if (pos < 0) {
            positions[currentPosition] = 0;
        } else {
            positions[currentPosition] = pos;
        }
        currentPosition++;
        }
        return positions;
    }
    }
}
